// PersonCaching.aj
//
// Informatics 102 Spring 2012
// Code Example
//
// This aspect introduces caching into the Person class.  It does so by
// mapping Person objects to CachedValue objects, where CachedValue objects
// contain a cached result and an expiration time.  The map itself is stored
// within the aspect and managed only by advice contained within the aspect.

import java.util.HashMap;
import java.util.Map;


public aspect PersonCaching
{
	// This map stores the values of toString() calls on particular Person
	// objects, along with an expiration time for each value.
	private Map<Person, CachedValue> toStringValues =
		new HashMap<Person, CachedValue>();
	

	// This pointcut matches against calls to the toString() method in
	// Person.  There are a couple of interesting things we're doing here:
	//
	// (1) We've included the @Cached annotation as part of the method's
	//     signature.  This is for two reasons: we want this aspect not
	//     to apply caching if the @Cached annotation is ever removed from
	//     the method, and we need the @Cached annotation to be on the
	//     method so we can extract it and pass it into the advice.
	//
	// (2) The @annotation() pointcut is used to capture the @Cached
	//     annotation from the method and pass it into a pointcut parameter,
	//     so it can be used within the advice.  This will allow the advice
	//     to get the timeToLiveMillis() value and use it to calculate an
	//     expiration time for the result.
	pointcut cachedToStringCalls(Person p, Cached cached):
		call(@Cached String Person.toString())
		&& target(p) && @annotation(cached);
	
	
	// We'll use around advice to replace toString() calls with cache
	// checking, where we see if a non-expired value is already available
	// in the cache and return that in lieu of calling the method.
	String around(Person p, Cached cached):
		cachedToStringCalls(p, cached)
	{
		// If there is already a value in the cache for the Person in
		// question, check if it has expired; if not, return it.
		if (toStringValues.containsKey(p))
		{
			CachedValue cachedValue = toStringValues.get(p);
			
			if (cachedValue.expirationTimeMillis <= System.currentTimeMillis())
			{
				return cachedValue.value;
			}
		}
		
		// If there is no value in the cache, or if the value in the
		// cache has expired, then we'll need to call the toString()
		// method and save the result in the cache before returning it.
		String value = proceed(p, cached);
		
		CachedValue cachedValue = new CachedValue(
			value, cached.timeToLiveMillis());
		
		toStringValues.put(p, cachedValue);
		
		return value;
	}
	
	
	// The CachedValue class combines a value with its expiration time,
	// expressed as a number of milliseconds since the Unix epoch -- the
	// same way that Java expresses the current time internally.
	private static class CachedValue
	{
		public String value;
		public long expirationTimeMillis;
		

		// When we create a CachedValue, we'll take the time to live and
		// add it to the current time to calculate its expiration time.
		public CachedValue(String value, int timeToLiveMillis)
		{
			this.value = value;

			expirationTimeMillis =
				System.currentTimeMillis() + timeToLiveMillis;
		}
	}
}
